/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */

import {
	type ApiDeclaredItem,
	type ApiItem,
	ApiItemKind,
	ReleaseTag,
} from "@microsoft/api-extractor-model";

import {
	getApiItemKind,
	getConciseSignature,
	getEffectiveReleaseLevel,
	getSingleLineExcerptText,
	isDeprecated,
	type ReleaseLevel,
} from "../../utilities/index.js";

import {
	getHierarchyConfigurationWithDefaults,
	type HierarchyConfiguration,
	type HierarchyOptions,
} from "./Hierarchy.js";

/**
 * Options for configuring the documentation suite generated by the API Item -\> Documentation Domain transformation.
 *
 * @public
 */
export interface DocumentationSuiteConfiguration {
	/**
	 * Whether or not to include a top-level heading in rendered documents.
	 *
	 * @defaultValue `true`
	 *
	 * @remarks If you will be rendering the document contents into some other document content that will inject its
	 * own root heading, this can be used to omit that heading from what is rendered by this system.
	 */
	readonly includeTopLevelDocumentHeading: boolean;

	/**
	 * Whether or not to include a navigation breadcrumb at the top of rendered documents.
	 *
	 * @defaultValue `true`
	 *
	 * @remarks Note: `Model` items will never have a breadcrumb rendered, even if this is specified.
	 */
	readonly includeBreadcrumb: boolean;

	/**
	 * {@link HierarchyConfiguration} to use for the provided API item.
	 */
	readonly hierarchy: HierarchyConfiguration;

	/**
	 * Optionally provide an override for the URI base used in links generated for the provided `ApiItem`.
	 *
	 * @remarks
	 *
	 * This can be used to match on particular item kinds, package names, etc., and adjust the links generated
	 * in the documentation accordingly.
	 *
	 * @param apiItem - The API item in question.
	 *
	 * @returns The URI base to use for the API item, or undefined if the default base should be used.
	 *
	 * @defaultValue Always use the default URI base.
	 */
	readonly getUriBaseOverrideForItem: (apiItem: ApiItem) => string | undefined;

	/**
	 * Generate heading text for the provided `ApiItem`.
	 *
	 * @param apiItem - The API item for which the heading is being generated.
	 *
	 * @returns The heading title for the API item.
	 *
	 * @defaultValue {@link DefaultDocumentationSuiteConfiguration.defaultGetHeadingTextForItem}
	 */
	readonly getHeadingTextForItem: (apiItem: ApiItem) => string;

	/**
	 * Generate link text for the provided `ApiItem`.
	 *
	 * @param apiItem - The API item for which the link is being generated.
	 *
	 * @returns The text to use in the link to the API item.
	 *
	 * @defaultValue {@link DefaultDocumentationSuiteConfiguration.defaultGetLinkTextForItem}
	 */
	readonly getLinkTextForItem: (apiItem: ApiItem) => string;

	/**
	 * Generate a list of "alerts" to display in API items tables for a given API item.
	 *
	 * @param apiItem - The API item for which table cell contents are being generated.
	 *
	 * @returns The list of "alert" strings to display.
	 *
	 * @defaultValue {@link DefaultDocumentationSuiteConfiguration.defaultGetAlertsForItem}
	 */
	readonly getAlertsForItem: (apiItem: ApiItem) => string[];

	/**
	 * Whether or not the provided API item should be excluded from documentation generation.
	 *
	 * @remarks Note: for items with children, excluding the item also results in the exclusion of all child items.
	 *
	 * @example Exclude packages with a particular name scope
	 *
	 * ```typescript
	 * excludeItem: (apiItem: ApiItem) => {
	 * 	if (apiItem.kind === ApiItemKind.Package) {
	 * 		return apiItem.displayName.startsWith("@private-scope/");
	 * 	}
	 * 	return false;
	 * }
	 * ```
	 *
	 * @example Exclude items tagged with custom `@skip` tag
	 *
	 * ```typescript
	 * excludeItem: (apiItem: ApiItem) => {
	 * 	return ApiItemUtilities.hasModifierTag(apiItem, "@skip");
	 * }
	 * ```
	 *
	 * @returns `true` if the item should be excluded from documentation generation. `false` otherwise.
	 *
	 * @defaultValue No items are skipped.
	 */
	readonly exclude: (apiItem: ApiItem) => boolean;

	/**
	 * Minimal release scope to include in generated documentation suite.
	 * API members with matching or higher scope will be included, while lower scoped items will be omitted.
	 *
	 * @remarks
	 *
	 * Note that items tagged as `@internal` are not included in the models generated by API-Extractor,
	 * so `@internal` items will never be included for such models.
	 *
	 * Hierarchy: `@public` \> `@beta` \> `@alpha` \> `@internal`
	 *
	 * @defaultValue Include everything in the input model.
	 *
	 * @example `@beta` and `@public`
	 *
	 * To only include `@beta` and `@public` items (and omit `@alpha` items), specify:
	 *
	 * ```typescript
	 * releaseLevel: ReleaseTag.Beta
	 * ```
	 */
	readonly minimumReleaseLevel: ReleaseLevel;
}

/**
 * Complete configuration documentation suite generation via the API Item -\> Documentation Domain transformation.
 *
 * @public
 */
export type DocumentationSuiteOptions = Omit<
	Partial<DocumentationSuiteConfiguration>,
	"hierarchy"
> & {
	/**
	 * {@inheritDoc DocumentationSuiteConfiguration.hierarchy}
	 */
	readonly hierarchy?: HierarchyOptions;
};

/**
 * Contains a list of default {@link DocumentationSuiteConfiguration} functions.
 *
 * @public
 */
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DefaultDocumentationSuiteConfiguration {
	/**
	 * Default {@link DocumentationSuiteConfiguration.getUriBaseOverrideForItem}.
	 *
	 * Always uses default URI base.
	 */
	export function defaultGetUriBaseOverrideForItem(): string | undefined {
		return undefined;
	}

	/**
	 * Default {@link DocumentationSuiteConfiguration.getHeadingTextForItem}.
	 *
	 * Uses the item's qualified API name, but is handled differently for the following items:
	 *
	 * - CallSignature, ConstructSignature, IndexSignature: Uses a cleaned up variation on the type signature.
	 *
	 * - Model: Uses "API Overview".
	 */
	export function defaultGetHeadingTextForItem(apiItem: ApiItem): string {
		const kind = getApiItemKind(apiItem);
		switch (kind) {
			case ApiItemKind.Model: {
				return "API Overview";
			}
			case ApiItemKind.CallSignature:
			case ApiItemKind.ConstructSignature:
			case ApiItemKind.IndexSignature: {
				// For signature items, the display-name is not particularly useful information
				// ("(constructor)", "(call)", etc.).
				// Instead, we will use a cleaned up variation on the type signature.
				const excerpt = getSingleLineExcerptText((apiItem as ApiDeclaredItem).excerpt);
				return trimTrailingSemicolon(excerpt);
			}
			default: {
				return apiItem.displayName;
			}
		}
	}

	/**
	 * Default {@link DocumentationSuiteConfiguration.getLinkTextForItem}.
	 *
	 * Uses the item's qualified API name, but is handled differently for the following items:
	 *
	 * - CallSignature, ConstructSignature, IndexSignature: Uses a cleaned up variation on the type signature.
	 *
	 * - Model: Uses "API Overview".
	 */
	export function defaultGetLinkTextForItem(apiItem: ApiItem): string {
		const itemKind = getApiItemKind(apiItem);
		switch (itemKind) {
			case ApiItemKind.Model: {
				return "Packages";
			}
			case ApiItemKind.CallSignature:
			case ApiItemKind.ConstructSignature:
			case ApiItemKind.IndexSignature: {
				// For signature items, the display-name is not particularly useful information
				// ("(constructor)", "(call)", etc.).
				// Instead, we will use a cleaned up variation on the type signature.
				const excerpt = getSingleLineExcerptText((apiItem as ApiDeclaredItem).excerpt);
				return trimTrailingSemicolon(excerpt);
			}
			default: {
				return getConciseSignature(apiItem);
			}
		}
	}

	/**
	 * Default {@link DocumentationSuiteConfiguration.getAlertsForItem}.
	 *
	 * Generates alerts for the following tags, if found:
	 *
	 * - `@alpha`: "Alpha"
	 *
	 * - `@beta`: "Beta"
	 *
	 * - `@deprecated`: "Deprecated"
	 */
	export function defaultGetAlertsForItem(apiItem: ApiItem): string[] {
		const alerts: string[] = [];
		if (isDeprecated(apiItem)) {
			alerts.push("Deprecated");
		}

		const releaseLevel = getEffectiveReleaseLevel(apiItem);
		if (releaseLevel === ReleaseTag.Alpha) {
			alerts.push("Alpha");
		} else if (releaseLevel === ReleaseTag.Beta) {
			alerts.push("Beta");
		}
		return alerts;
	}

	/**
	 * Default {@link DocumentationSuiteConfiguration.exclude}.
	 *
	 * Unconditionally returns `false` (i.e. no packages will be filtered out).
	 */
	export function defaultExclude(): boolean {
		return false;
	}
}

/**
 * Gets a complete {@link DocumentationSuiteConfiguration} using the provided partial configuration, and filling
 * in the remainder with the documented defaults.
 */
export function getDocumentationSuiteConfigurationWithDefaults(
	options?: DocumentationSuiteOptions,
): DocumentationSuiteConfiguration {
	const hierarchy: HierarchyConfiguration = getHierarchyConfigurationWithDefaults(
		options?.hierarchy,
	);

	return {
		hierarchy,
		includeTopLevelDocumentHeading: options?.includeTopLevelDocumentHeading ?? true,
		includeBreadcrumb: options?.includeBreadcrumb ?? true,
		getUriBaseOverrideForItem:
			options?.getUriBaseOverrideForItem ??
			DefaultDocumentationSuiteConfiguration.defaultGetUriBaseOverrideForItem,
		getHeadingTextForItem:
			options?.getHeadingTextForItem ??
			DefaultDocumentationSuiteConfiguration.defaultGetHeadingTextForItem,
		getLinkTextForItem:
			options?.getLinkTextForItem ??
			DefaultDocumentationSuiteConfiguration.defaultGetLinkTextForItem,
		getAlertsForItem:
			options?.getAlertsForItem ??
			DefaultDocumentationSuiteConfiguration.defaultGetAlertsForItem,
		exclude: options?.exclude ?? DefaultDocumentationSuiteConfiguration.defaultExclude,
		minimumReleaseLevel: options?.minimumReleaseLevel ?? ReleaseTag.Internal,
	};
}

/**
 * Trims a trailing semicolon from the provided text, if the text contains one.
 */
function trimTrailingSemicolon(text: string): string {
	if (text.endsWith(";")) {
		return text.slice(0, text.length - 1);
	}
	return text;
}
